extends Node2D

var SIDE = 10
var STEP = 50

var TICKS_PER_TEST = 100
var WALL_COLOR = Color8(255,255,255,255)
var WALL_THICKNESS = 3

var TRIALS_PER_INDIVIDUUM = 10

#scoring

var COOL_NEIGHBOR_BONUS = 60
var ROOM_MAX_SIZE = 6
var ROOM_MIN_SIZE = 4
var CORRIDOR_MAX_SIZE = SIDE * 2

var MINIMUM_CHAMBERS = 2

var ROOM_BONUS = 20.0
var CORRIDOR_BONUS = 0.5

var INVALID_PENALTY = 0.95

var COOL_COMBOS = [
	[CellType.types.CORRIDOR, CellType.types.ROOM],
	[CellType.types.ROOM, CellType.types.ROOM],
]

var POP_SIZE = 6 #must be an even number

var cells = []
var visual_cells = []
var postview_cells = []
var rooms = []
var changes = 0

var population = []

var simulation_timer = Timer.new()
var SIMULATION_TICK_DURATION = 0.3

var walls = []
var wall_node = null

var worker_thread
var stop_worker = false

var POSTVIEW_UPDATE_EVERY = 3 #... trials
var postview_counter = 0 # a counter to determine 1/x frames that will be shown

onready var LAST_SCORE = find_node("LAST_SCORE")
onready var BEST_SCORE = find_node("BEST_SCORE")
onready var GENERATION_LABEL = find_node("GENERATION")
onready var SCORE_TABLE = find_node("SCORE_TABLE")
onready var LAST_THING = find_node("LAST_THING")
onready var LAYER_SELECTOR = find_node("LAYER_SELECTOR")

func _ready():
	get_initial_pop()
	prepare_visual_cells()

func prepare_visual_cells():

	for i in range(SIDE):
		for j in range(SIDE):
			var c = VisualCell.new()
			add_child(c)
			c.rect_position = Vector2(i,j) * STEP
			c.rect_size = Vector2.ONE * STEP
			visual_cells[i][j] = c

	for i in range(SIDE):
		for j in range(SIDE):
			var c = ColorRect.new()
			LAST_THING.add_child(c)
			c.rect_position = Vector2(i,j) * STEP
			c.rect_size = Vector2.ONE * STEP
			postview_cells[i][j] = c
	pass

func start_evolution(foreground = true):
	if foreground:
		print("starting foreground evolution")
		evolve(10)
	else:
		worker_thread = Thread.new()
		worker_thread.start(self,"evolve",0)

#	worker_thread.start(self,"test_individuum", population[0])
#	call_deferred("test_individuum",population[0])
#	print(test_individuum (population[0]))
	#start_ticking()

func evolve(generation_count):
	print("starting evolution")
	var elapsed_generations = 0

	while true:
		var has_nonzero = false
		var reevaluate = (elapsed_generations !=0 and elapsed_generations % 30 == 0)
		for i in range(population.size()):
			if stop_worker:
				stop_worker = false
				return
			if population[i].score == -1 or reevaluate:
				test_individuum(population[i],TRIALS_PER_INDIVIDUUM if !reevaluate else TRIALS_PER_INDIVIDUUM*3)
				SCORE_TABLE.update_scores()
			if population[i].score > 0:
				has_nonzero = true
			elif i > POP_SIZE/2 - 1:
				if population[i-POP_SIZE/2].score != 0:
					print ("failed reproduction of results")
					population[i-POP_SIZE/2].gentle_divisor *= 2.0
					print("new divisor is ", population[i-POP_SIZE/2].gentle_divisor)
				else:
					print ("successful reproduction")
					population[i-POP_SIZE/2].gentle_divisor /= 1.1

		if has_nonzero:
			population.sort_custom(self, "score_comparator")
			population.invert()
		else:
			population.shuffle() #allow drifting if no actual scores exist

		BEST_SCORE.update_score(population[0].score)
		print ("best score is ", population[0].score)
		assert(POP_SIZE % 2 == 0)
		population = population.slice(0,POP_SIZE/2-1)
		elapsed_generations += 1
		print("generation number ", elapsed_generations)
		GENERATION_LABEL.update_generation(elapsed_generations)
		if generation_count!=0:
			if elapsed_generations > generation_count:
				print("Success!")
				break


		if stop_worker:
			stop_worker = false
			break

		for i in range(POP_SIZE/2):
			population.append(population[i].mutate(has_nonzero and population[i].score))

func score_comparator (a,b):
	if a.score < b.score:
		return true
	return false

func test_individuum(individuum, trials):
	var score = 0
	for i in range(trials):
		score += test_individuum_once(individuum)
		if stop_worker:
			print("breaking on a signal")
			break
	if not stop_worker:
		print("ending testing individuum")
		score /= trials
		individuum.score = score
		LAST_SCORE.update_score(score)
		print("last score was ", score)

func test_individuum_once(individuum):

	var update_postview = false

	postview_counter += 1

	if postview_counter % POSTVIEW_UPDATE_EVERY == 0:
		postview_counter = 0
		update_postview = true


	print("clearing")
	clear_grid()
	print("populating")
	populate_random(individuum)
	print("populated. filling neighbors")
	fill_neighbors()
	print("starting ticks")
	for i in range(TICKS_PER_TEST):
		simulation_tick()

	if update_postview:
		print("clearing walls")
		clear_walls()

	print("segregating")
	segregate_rooms(update_postview)

	if update_postview:
		print("after segregation there are ", walls.size(), " walls. syncing")
		sync_colors()
	print("scoring")
	var score = calculate_score()
	print ("intermediate score: ", score)
	if score == null:
		push_error("null score!")
	return score


func calculate_score():
	var floor_area = SIDE*SIDE
	var score = 0.0
	var invalid_rooms = []

	var ch = int(changes)
	print ("algorithm made ", ch, "changes")
	score = float(changes) * 0.2
	print ("score right after changes", score)

	if rooms.size() < MINIMUM_CHAMBERS:
		score = 0.0
		print("too few chambers, discarding")
		return score

#	score = 2

	if changes == 0:
		score = 0.0
		print("no changes, discarding")
		return score

	var room_count = 0
	var corridor_count = 0

	print ("there are ", rooms.size(), " rooms")

	if rooms.size() == SIDE*SIDE:
		score = 0.0
		print("regular grid, discarding")
		return score

	for r in rooms:
		if r == null:
			print ("null room!")

		var room_neighbors = []
		#print("counting neighbors")
		for c in r.neighbor_cells:
			#print("checking cell")
			if not c.room in room_neighbors:
				#print("adding neighbor")
				room_neighbors.append(c.room)

		#print ("checking type")

		var room_type = r.type
		assert (room_type)
		#print (room_type)

		#print ("checking type index")
		#print(room_type.type_index)
		var r_type_ind = room_type.type_index

		# Roomsize score
		#print ("checking room size")
		if r_type_ind == CellType.types.ROOM:
			room_count += 1
			if room_validator(r):
				score += ROOM_BONUS
				#print("good room! score: ", score)
			else:
				invalid_rooms.append(r)
				continue

		#print ("checking corridor size")
		if r_type_ind == CellType.types.CORRIDOR:
			corridor_count += 1
			if corridor_validator(r):
				score += CORRIDOR_BONUS * r.cells.size()
				score += ROOM_BONUS
				#print("added for corridor length. score: ", score)
			else:
				invalid_rooms.append(r)
				continue

		# neighbour score
		#print ("checking neighbors")
		var cool_neighbors = 0
		for rn in room_neighbors:
			var rn_type_ind = rn.type.type_index

			var valid_neighbor = false
			if rn_type_ind == CellType.types.CORRIDOR:
				valid_neighbor = corridor_validator(rn)
			if rn_type_ind == CellType.types.ROOM:
				valid_neighbor = room_validator(rn)

			if valid_neighbor:
				for combo in COOL_COMBOS:
					combo.sort()
					var types = [rn_type_ind, r_type_ind]
					types.sort()
					var nocombo = false
					for i in range(types.size()):
						if types[i] != combo[i]:
							nocombo = true
					if !nocombo:
						score += COOL_NEIGHBOR_BONUS / 2 #to prevent double-counting
						cool_neighbors += 1
						#print("cool neighbor found, bonus! score: ", score)

		if r_type_ind == CellType.types.CORRIDOR:
			score += ROOM_BONUS * (pow(2,cool_neighbors) - 1) # role of a corridor is to connect. it is an endgoal so no bonus is too high ^_^


	print ("invalid rooms: ", invalid_rooms.size())
	print ("checking if enough rooms")
	if corridor_count == 0 or room_count == 0:
			score = 0
			print("no rooms or no corridors")
			return score

	print ("score before change modifier", score)
	score *= sqrt(float(changes)/float(floor_area))

	print ("score before invalid room count penalty", score)
	score = score * pow(INVALID_PENALTY,invalid_rooms.size())
	print ("score before invalid room area penalty", score)
	var invalidated_area = 0
	for r in invalid_rooms:
		invalidated_area += r.cells.size()

	var area_penalizer = (1-(float(invalidated_area)/float(SIDE*SIDE)))

	if area_penalizer == 0: #it is a non-discarding penalizer
		area_penalizer = 1.0/(SIDE*SIDE)
	score = score * area_penalizer

	print ("end reached, returning", score)

	return score

func _init():
	for arr in [cells,visual_cells, postview_cells]:
		for i in range(SIDE):
			arr.append([])
			for j in range(SIDE):
				arr[i].append(null)


func room_validator(room):
	if room.cells.size() >= ROOM_MIN_SIZE and room.cells.size() <= ROOM_MAX_SIZE:
		return true
	return false

func corridor_validator(corridor):
	if corridor.cells.size() >= ROOM_MIN_SIZE and corridor.cells.size() <= CORRIDOR_MAX_SIZE:
		return true
	return false

func populate_random(individuum):

	randomize()
#	for line in individuum.types[0].matrix:
#		print (line)
#	print ("own production is \n", individuum.types[0].own_production)
#	for t in individuum.types:
#		for p in t.proliferations:
#			print("min proliferations\n", p.min_ranges)
#			print("max proliferations\n", p.max_ranges)
	print("populating")
	for i in range(SIDE):
		for j in range(SIDE):
			var c = Cell.new()
			c.type = individuum.types[randi() % CellType.types.size()]
			if c.type == null:
				print("null type!")
			c.individuum = individuum
			c.iii = i
			c.jjj = j
			#print (c.type)
			#c.update_color()
			cells[i][j] = c
			visual_cells[i][j].cell = c


func clear_grid():
	for i in range(SIDE):
		for j in range(SIDE):
			visual_cells[i][j].cell = null
			cells[i][j].free()
			cells[i][j] = null #garbage collector will do the rest
	rooms.clear()
	changes = 0.0

func clear_walls():
#	for w in walls:
#		w.queue_free()
	wall_node.queue_free()
	wall_node = 0
	walls.clear()

func get_initial_pop():
	var adam = Individuum.new()
	for i in range(POP_SIZE):
		population.append(adam.mutate())

func fill_neighbors():
	for i in range(SIDE):
		for j in range(SIDE):
			var neighbors = []
			for ii in [i+1,i-1]:
				if ii >= 0 and ii < SIDE:
					neighbors.append(cells[ii][j])
			for jj in [j+1,j-1]:
				if jj >= 0 and jj < SIDE:
					neighbors.append(cells[i][jj])

			cells[i][j].neighbors = neighbors
#			print("adding ", neighbors.size(), " neighbors")

func segregate_rooms(draw_walls = false):
	var history = []

	if draw_walls:
		wall_node = Node2D.new()
		add_wall_around_grid()

	for i in cells:
		for c in i:
			var room = Room.new()
			roomba(history, c, room, draw_walls)
			if room.cells.size() !=0:
				rooms.append(room)
	for r in rooms:
		var room_type = r.cells[0].type
		if room_type == null:
			print ("room type is null!")
		assert (room_type)
		r.type = room_type
	if draw_walls:
		LAST_THING.call_deferred("add_child",wall_node)

func sync_colors():
	for i in range(SIDE):
		for j in range(SIDE):
			postview_cells[i][j].color = visual_cells[i][j].color
			postview_cells[i][j].color.a = 0.5

func roomba(history,cell,room, draw_walls = false):
	if cell in history:
		return

	room.cells.append(cell)
	cell.room = room
	history.append(cell)

	for n in cell.neighbors:
		if !check_different_rooms(n,cell):
			roomba(history, n, room, draw_walls)
		else:
			if draw_walls:
				add_wall(n,cell)
			room.neighbor_cells.append(n)

	return history

func add_wall(a,b): #a visible wall between two cells

	var visual_a = visual_cells[a.iii][a.jjj]
	var visual_b = visual_cells[b.iii][b.jjj]

	var center_a = visual_a.rect_position + Vector2.ONE * visual_a.rect_size/2.0
	var center_b = visual_b.rect_position + Vector2.ONE * visual_b.rect_size/2.0

	var line = wall_between(center_a,center_b,STEP)
	walls.append(line)
#	LAST_THING.add_child(line)
	wall_node.add_child(line)


func wall_between(vec1, vec2, length):
	var line = Line2D.new()
	line.default_color = WALL_COLOR
	line.width = WALL_THICKNESS
	line.z_index = 10
	var pos_diff = vec1 - vec2
	if pos_diff.length() > STEP: #not enough?
		print("WALL OOPS")
		stop_worker = true
	var centerpoint = vec2 + pos_diff/2.0
	var normal = Vector2(pos_diff.y, -pos_diff.x).normalized()*length/2.0

	for v in [-normal,normal]:
		line.add_point(centerpoint+v)

	return line

func add_wall_around_grid():
	var line = Line2D.new()
	line.default_color = WALL_COLOR
	line.width = WALL_THICKNESS
	var grid_side = SIDE*STEP
	line.add_point(Vector2(0,0))
	line.add_point(Vector2(grid_side,0))
	line.add_point(Vector2(grid_side,grid_side))
	line.add_point(Vector2(0,grid_side))
	line.add_point(Vector2(0,0))
	walls.append(line)
	wall_node.add_child(line)
#	LAST_THING.add_child(line)

func check_different_rooms(a,b):
	if a.type != b.type:
		return true

	var room_signals =[]

	#first 2 signals have a special predetermined purpose: they mark rooms.
	# there are 2 "bits" for demarkation and this should be enough because of graph coloring theorem.
	for cell in [a,b]:
		room_signals.append( Vector2(sign(cell.field_vector[0]),sign(cell.field_vector[1])))
	if room_signals[0] != room_signals[1]:
		return true

	return false


func start_ticking():
	simulation_timer.wait_time = SIMULATION_TICK_DURATION
	add_child(simulation_timer)
	simulation_timer.connect("timeout",self,"simulation_tick")
	simulation_timer.start()

func simulation_tick():
	#print("tick!")
	for i in cells:
		for c in i:
			c.calculate_field()
	for i in cells:
		for c in i:
			c.apply_field()
	for i in cells:
		for c in i:
			if c.proliferate():
				changes += 1
	for i in visual_cells:
			for c in i:
				c.update_color()
	LAYER_SELECTOR.highlight_selected()

#func _exit_tree():
#	if worker_thread:
#		stop_worker = true
#		worker_thread.wait_to_finish()
